package com.googlecode.agileplugins.monkey;

import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInspection.InspectionManager;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiArrayInitializerExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassObjectAccessExpression;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiIdentifier;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.UsageSearchContext;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MockExpectsCheckingInspection extends LocalInspectionTool {
    protected Logger logger = Logger.getInstance(getClass().getName());

    String mockName;

    @NotNull
    public String getGroupDisplayName() {
        return "PurpleMonkeyDishwasher";
    }

    @NotNull
    public String getDisplayName() {
        return "Check expectOneCallTo calls";
    }

    @NotNull
    public String getShortName() {
        return "MockExpectsChecker";
    }

    public boolean isEnabledByDefault() {
        return true;
    }

    @NotNull
    public HighlightDisplayLevel getDefaultLevel() {
        return HighlightDisplayLevel.ERROR;
    }

    public ProblemDescriptor[] checkMethod(@NotNull PsiMethod method, @NotNull InspectionManager manager, boolean isOnTheFly) {
        List<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>();
        checkForMethodCalls(method.getManager(), "expectOneCallTo", method, problems, manager);
        checkForMethodCalls(method.getManager(), "expectAtLeastOneCallTo", method, problems, manager);
        checkForMethodCalls(method.getManager(), "expectOneOrderedCallTo", method, problems, manager);
        return problems.toArray(new ProblemDescriptor[problems.size()]);
    }

    private void checkForMethodCalls(PsiManager psiManager, String methodName, PsiMethod method, List<ProblemDescriptor> problems, InspectionManager manager) {
        PsiIdentifier[] identifiers = psiManager.getSearchHelper().findIdentifiers(methodName, new LocalSearchScope(method), UsageSearchContext.IN_CODE);
        for (PsiIdentifier identifier : identifiers) {
            checkExpectation(identifier, psiManager, problems, manager);
        }
    }

    private void checkExpectation(PsiIdentifier identifier, PsiManager psiManager, List<ProblemDescriptor> problems, InspectionManager manager) {
        PsiElement element = identifier.getParent();
        if (element instanceof PsiReferenceExpression) {
            PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression) element;
            PsiElement psiElement = psiReferenceExpression.getParent();
            if (psiElement instanceof PsiMethodCallExpression) {
                PsiMethodCallExpression psiMethodCallExpression = (PsiMethodCallExpression) psiElement;
                PsiReferenceExpression methodExpression = psiMethodCallExpression.getMethodExpression();
                System.out.println("methodExpression = " + methodExpression);
                PsiExpressionList list = psiMethodCallExpression.getArgumentList();
                System.out.println("list = " + Arrays.asList(list.getExpressions()));
                PsiExpression[] psiExpressions = list.getExpressions();
                if (psiExpressions.length == 2) {
                    PsiExpression methodNameExpression = psiExpressions[0];
                    PsiExpression mockObjectExpression = psiExpressions[1];
                    if (methodNameExpression instanceof PsiLiteralExpression) {
                        PsiLiteralExpression psiLiteralExpression = (PsiLiteralExpression) methodNameExpression;
                        String methodName = (String) psiLiteralExpression.getValue();
                        System.out.println("methodName = " + methodName);
                        PsiType type = mockObjectExpression.getType();
                        System.out.println("type = " + type);
                        if (type == null) {
                            return;
                        }
                        PsiClass mockClass = psiManager.findClass(type.getCanonicalText(), GlobalSearchScope.allScope(manager.getProject()));
                        if (mockClass == null) {
                            return;
                        }
                        PsiMethod[] psiMethods = mockClass.findMethodsByName(methodName, true);
                        if (psiMethods.length == 0) {
                            problems.add(manager.createProblemDescriptor(psiLiteralExpression, "Class " + type.getPresentableText() +
                                    " does not contain method " + methodName, (LocalQuickFix) null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
                            return;
                        }
                        Project project = manager.getProject();
                        PsiElement nextParam = psiMethodCallExpression.getParent().getParent();
                        PsiType[] params = getParameters(nextParam);
                        PsiElement returnValue = nextParam.getParent().getParent();
                        PsiType returnType = getReturnType(returnValue);
                        System.out.println("returnValue = " + returnValue);
                        System.out.println("returnType = " + returnType);
                        PsiType methodReturnType = null;
                        if (params.length > 0) {
                            boolean foundMatch = false;
                            for (int i = 0; i < psiMethods.length; i++) {
                                PsiMethod psiMethod = psiMethods[i];
                                PsiParameterList psiParameterList = psiMethod.getParameterList();
                                PsiParameter[] parameters = psiParameterList.getParameters();
                                foundMatch = checkParameters(parameters, params, project);
                                if (foundMatch) {
                                    methodReturnType = psiMethod.getReturnType();
                                    break;
                                }
                            }
                            if (!foundMatch) {
                                problems.add(manager.createProblemDescriptor(((PsiMethodCallExpression) nextParam).getArgumentList(), getMessage(type, methodName, params), (LocalQuickFix) null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
                            }
                        }
                        if (returnType != null && methodReturnType != null && !methodReturnType.isAssignableFrom(returnType)) {
                            problems.add(manager.createProblemDescriptor(((PsiMethodCallExpression) returnValue).getArgumentList(), getReturnMessage(type, methodName, returnType, methodReturnType), (LocalQuickFix) null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
                        }
                    }
                }
            }
        }
    }

    private PsiType getReturnType(PsiElement returnValue) {
        if (returnValue instanceof PsiMethodCallExpression) {
            PsiMethodCallExpression willExpression = (PsiMethodCallExpression) returnValue;
            PsiReferenceExpression psiReferenceExpression = willExpression.getMethodExpression();
            String name = psiReferenceExpression.getReferenceName();
            if ("will".equals(name)) {
                PsiExpressionList list = willExpression.getArgumentList();
                PsiExpression[] psiExpressions = list.getExpressions();
                if (psiExpressions.length == 1) {
                    PsiExpression psiExpression = psiExpressions[0];
                    if (psiExpression instanceof PsiMethodCallExpression) {
                        PsiMethodCallExpression psiMethodCallExpression = (PsiMethodCallExpression) psiExpression;
                        PsiReferenceExpression methodExpression = psiMethodCallExpression.getMethodExpression();
                        String name1 = methodExpression.getReferenceName();
                        if ("returnValue".equals(name1)) {
                            PsiExpressionList expressionList = psiMethodCallExpression.getArgumentList();
                            PsiExpression[] psiExpressions1 = expressionList.getExpressions();
                            if (psiExpressions1.length == 1) {
                                return psiExpressions1[0].getType();
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    private PsiType[] getParameters(PsiElement parent) {
        List<PsiType> params = new ArrayList<PsiType>();
        if (parent instanceof PsiMethodCallExpression) {
            PsiMethodCallExpression withOrWillExpression = (PsiMethodCallExpression) parent;
            PsiReferenceExpression referenceExpression = withOrWillExpression.getMethodExpression();
            String nextMethodName = referenceExpression.getReferenceName();
            if ("with".equals(nextMethodName)) {
                PsiExpressionList list = withOrWillExpression.getArgumentList();
                PsiExpression[] psiExpressions = list.getExpressions();
                if (psiExpressions.length == 1) {
                    PsiExpression psiExpression = psiExpressions[0];
                    PsiType type = psiExpression.getType();
                    String s = type == null ? "" : type.getPresentableText();
                    if (s.endsWith("Constraint[]")) {
                        if (psiExpression instanceof PsiNewExpression) {
                            PsiNewExpression psiNewExpression = (PsiNewExpression) psiExpression;
                            PsiArrayInitializerExpression psiArrayInitializerExpression = psiNewExpression.getArrayInitializer();
                            if (psiArrayInitializerExpression != null) {
                                psiExpressions = psiArrayInitializerExpression.getInitializers();
                            }
                        } else if (psiExpression instanceof PsiReferenceExpression) {
                            PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression) psiExpression;
                            PsiElement psiElement = psiReferenceExpression.resolve();
                            if (psiElement instanceof PsiLocalVariable) {
                                PsiLocalVariable psiLocalVariable = (PsiLocalVariable) psiElement;
                                PsiExpression expression = psiLocalVariable.getInitializer();
                                if (expression instanceof PsiArrayInitializerExpression) {
                                    PsiArrayInitializerExpression psiArrayInitializerExpression = (PsiArrayInitializerExpression) expression;
                                    psiExpressions = psiArrayInitializerExpression.getInitializers();
                                } else if (expression instanceof PsiNewExpression) {
                                    PsiNewExpression psiNewExpression = (PsiNewExpression) expression;
                                    PsiArrayInitializerExpression psiArrayInitializerExpression = psiNewExpression.getArrayInitializer();
                                    if (psiArrayInitializerExpression != null) {
                                        psiExpressions = psiArrayInitializerExpression.getInitializers();
                                    }
                                }
                            }
                        }
                    }
                }
                for (PsiExpression psiExpression : psiExpressions) {
                    if (psiExpression instanceof PsiMethodCallExpression) {
                        PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression) psiExpression;
                        String name = methodCallExpression.getMethodExpression().getReferenceName();
                        PsiExpression[] paramExpressions = methodCallExpression.getArgumentList().getExpressions();
                        if (paramExpressions.length == 1 && ("eq".equals(name) || "same".equals(name))) {
                            params.add(paramExpressions[0].getType());
                            continue;
                        }
                    }
                    params.add(null);
                }
            }
        }
        return params.toArray(new PsiType[params.size()]);
    }

    private boolean checkParameters(PsiParameter[] parameters, PsiType[] paramTypes, Project project) {
        if (parameters.length == paramTypes.length) {
            for (int i = 0; i < parameters.length; i++) {
                PsiParameter parameter = parameters[i];
                if (paramTypes[i] == null) {
                    continue;
                }
                PsiType paramType = parameter.getType();
                if (!paramType.isAssignableFrom(paramTypes[i])) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    private String getReturnMessage(PsiType type, String methodName, PsiType methodReturnType, PsiType returnType) {
        return "Cannot return " + type + " from " + methodReturnType + " " + type.getCanonicalText() + "." + methodName + "()";
    }

    private String getMessage(PsiType type, String methodName, PsiType[] paramTypes) {
        StringBuffer sb = new StringBuffer();
        sb.append(type.getCanonicalText()).append('.').append(methodName).append("()");
        sb.append(" does not have a");
        if (paramTypes.length == 1) {
            sb.append(" signature of type ").append(getTypeName(paramTypes[0]));
        } else {
            sb.append(" signature with types ");
            appendTypes(paramTypes, sb);
        }
        return sb.toString();
    }

    private void appendTypes(PsiType[] paramTypes, StringBuffer sb) {
        for (int i = 0; i < paramTypes.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(getTypeName(paramTypes[i]));
        }
    }

    private String getTypeName(PsiType paramType) {
        return paramType == null ? "unknown" : paramType.getCanonicalText();
    }

    private PsiType[] getTypes(PsiExpression params, boolean classes) {
        if (params instanceof PsiReferenceExpression) {
            PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression) params;
            PsiElement psiElement = psiReferenceExpression.resolve();
            if (psiElement instanceof PsiLocalVariable) {
                PsiLocalVariable psiLocalVariable = (PsiLocalVariable) psiElement;
                params = psiLocalVariable.getInitializer();
            } else if (psiElement instanceof PsiField) {
                PsiField psiField = (PsiField) psiElement;
                params = psiField.getInitializer();
            }
        }
        if (params instanceof PsiClassObjectAccessExpression && classes) {
            PsiClassObjectAccessExpression psiClassObjectAccessExpression = (PsiClassObjectAccessExpression) params;
            PsiType type = psiClassObjectAccessExpression.getOperand().getType();
            return new PsiType[]{type};
        }
        if (params instanceof PsiNewExpression) {
            PsiNewExpression psiNewExpression = (PsiNewExpression) params;
            params = psiNewExpression.getArrayInitializer();
            if (params == null && !classes) {
                return new PsiType[]{psiNewExpression.getType()};
            }
        }
        if (params instanceof PsiArrayInitializerExpression) {
            PsiArrayInitializerExpression arrayInitializer = (PsiArrayInitializerExpression) params;
            PsiExpression[] initializers = arrayInitializer.getInitializers();
            PsiType[] paramTypes = new PsiType[initializers.length];
            for (int k = 0; k < initializers.length; k++) {
                PsiExpression initializer = initializers[k];
                if (initializer instanceof PsiClassObjectAccessExpression && classes) {
                    PsiClassObjectAccessExpression psiClassObjectAccessExpression = (PsiClassObjectAccessExpression) initializer;
                    paramTypes[k] = psiClassObjectAccessExpression.getOperand().getType();
                } else if (!classes) {
                    paramTypes[k] = initializer.getType();
                }
            }
            return paramTypes;
        }
        if (params != null) {
            return new PsiType[]{params.getType()};
        }
        return null;
    }

//    private String mockString;
//
//    private void xxx() {
//        if (Math.random()==1) {
//            expectOneCallTo("xxx", mockString).with(eq("one"));
//        }
//        expectOneCallTo("toString", mockString).will(returnValue("hello"));
//        expectOneCallTo("lastIndexOf", mockString).with(eq("one"), eq(42)).will(returnValue(12));
//        String stupid = "";
//        expectOneCallTo("contains", mockString).with(same("one")).will(returnValue(true));
//    }
//
//    private Object returnValue(Object s) {
//        throw new UnsupportedOperationException();
//    }
//
//    private Constraint eq(Object o) {
//        throw new UnsupportedOperationException();
//    }
//
//    private Constraint same(Object o) {
//        throw new UnsupportedOperationException();
//    }
//
//    private Blah expectOneCallTo(String s, Object p1) {
//        throw new UnsupportedOperationException();
//    }
//
//
//    class Blah {
//
//        public Blah with(Constraint p0) {
//            throw new UnsupportedOperationException();
//        }
//        public Blah with(Constraint p0, Constraint p1) {
//            throw new UnsupportedOperationException();
//        }
//        public Blah with(Constraint[] p0) {
//            throw new UnsupportedOperationException();
//        }
//
//        public void will(Object p0) {
//            throw new UnsupportedOperationException();
//        }
//
//    }
//
//    class Constraint {
//
//    }
}
